package simulation;

import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * Authors: Kyle Martin, Ross Hadden, Ben Lee Purpose: Control class. Generates
 * the simulation, processes each event as it happens. Version 2 Last updated:
 * 11/22/11 Tested by: Tested on:
 */
public class Simulation {

	public boolean running;
	private SimulationDisplay gui;

	public Queue takeoffQueue;
	public Queue landingQueue;

	public Runway[] Runways;

	int timeCounter = 0;
	public PriorityQueue<Event> eventPriorityQueue;

	private int random;

	private WeatherType currentWeather = WeatherType.SUN;
	final int SUN_TIME = 2;
	final int RAIN_TIME = 3;
	final int WIND_TIME = 8;
	final int SNOW_TIME_START = 6;
	final int SNOW_5_TIME = 8; // After 5 minutes
	final int SNOW_10_TIME = 10; // After 10 minutes

	private int totalDelays = 0;

	private int simScore;

	/**
	 * Authors: Kyle Martin, Ross Hadden, Ben Lee Purpose: Constructor.
	 * Generates planes and weather. Creates GUI. Begins game loop. Tested by:
	 * Tested on:
	 */
	public Simulation() {
		running = true;

		takeoffQueue = new Queue();
		landingQueue = new Queue();

		Runways = new Runway[4];
		for (int i = 0; i < Runways.length; i++) {
			Runways[i] = new Runway();
		}

		Comparator<Event> comparator = new EventTimeComparator();
		eventPriorityQueue = new PriorityQueue<Event>(10, comparator);

		int duration = 30; // 30minute duration
		//Plane generation
		for (int i = 1; i < 20; i++) {
			random = (int) Math.floor(Math.random() * 10) + 1;
			if (random % 2 == 0) {
				eventPriorityQueue.add(new Plane(i * random, Type.LANDING));
				// System.out.println("LANDING Added." + i * random);
			} else {
				eventPriorityQueue.add(new Plane(i * random, Type.TAKEOFF));
				// System.out.println("TAKEOFF Added." + i * random);
			}
		}
		for (int i = 1; i < 20; i++) {
			random = (int) Math.floor(Math.random() * 10) + 1;
			if (random % 4 == 0) {
				eventPriorityQueue
						.add(new Weather(i * random, WeatherType.SUN));
			} else if (random % 6 == 1) {
				eventPriorityQueue
						.add(new Weather(i * random, WeatherType.SNOW));
			} else if (random % 6 == 2) {
				eventPriorityQueue
						.add(new Weather(i * random, WeatherType.WIND));
			}
			 else if (random % 7 == 2) {
					eventPriorityQueue
							.add(new Weather(i * random, WeatherType.RAIN));
				}
		}

		simScore = generateSimScore();
		System.out.println("Score to beat: " + simScore);

		gui = new SimulationDisplay(this);
		gui.updateRunways(Runways);

		while (true) {
			loop();
		}
	}

	/**
	 * Authors: Kyle Martin, Ross Hadden, Ben Lee Purpose: Constructor. Creates
	 * a simulation with the given sequence of events. Does not construct the
	 * GUI Used for scoring. Tested by: Tested on:
	 */
	public Simulation(PriorityQueue<Event> q) {
		// Initialize variables, nothing else.
		this.eventPriorityQueue = q;
		running = true;

		takeoffQueue = new Queue();
		landingQueue = new Queue();

		Runways = new Runway[4];
		for (int i = 0; i < Runways.length; i++) {
			Runways[i] = new Runway();
		}
	}

	/**
	 * Authors: Kyle Martin, Ross Hadden Purpose: (Re)starts the game loop.
	 * Tested by: Tested on:
	 */
	public void start() {
		if (!running) {
			System.out.println("Starting.");
			running = true;
			loop();
		}
	}

	/**
	 * Authors: Kyle Martin, Ross Hadden Purpose: Stops the game loop. Tested
	 * by: Tested on:
	 */
	public void stop() {
		if (running) {
			System.out.println("Stopping.");
			running = false;
		}
	}

	/**
	 * Authors: Kyle Martin, Ross Hadden Purpose: Called in infinite loop by
	 * constructor. Handles time delay. Tested by: Tested on:
	 */
	public void loop() {
		if (running) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

			update();

		}
	}

	/**
	 * Authors: Kyle Martin Purpose: Runs through one step of the simulation.
	 * Tested by: Tested on:
	 */
	public void update() {

		timeCounter += 1;
		if(gui != null)
			gui.changeSchedule.updateTime(timeCounter);
		
		//We are done when there are no planes left.  If there are no planes or runway events left, we don't care 
		//about the upcoming weather.
		boolean done = true;
		for(Event e: eventPriorityQueue){
			if(e instanceof Plane || e instanceof RunwayEvent){  
				done = false;
			}
		}
		if(done || eventPriorityQueue.peek() == null){
			System.out.println("Empty Queue");
			if(gui != null)
				gui.score.showFinalScore();
			running = false;
			return;
		}

		Event e = eventPriorityQueue.peek();
//		System.out.println(timeCounter + "  / " + e.getEventTime());

		while (e.getEventTime() <= timeCounter) {

			// System.out.println("TimeCounter:"+timeCounter + " / EventTime:" +
			// e.getEventTime());
			// System.out.println("QueueSize: " +eventPriorityQueue.size());
			e = eventPriorityQueue.poll();

			if (e instanceof Plane) {
				Plane plane = (Plane) e;

				// Airport is closed after 10 minutes of snow
				if (currentWeather == WeatherType.SNOW_10) {
					totalDelays += 60;
					if (gui != null) { // For the scoring simulation, gui =
										// null;
						gui.changeSchedule.removePlane(plane);
						gui.score.updateScore();
					}
					System.out.println(timeCounter + ": " +"Airport closed, plane diverted!");
					continue;
				}

				// Go through the runways, looking for an open one.
				boolean placed = false;
				for (Runway r : Runways) {
					if (r.getStatus() == true && r.getPlaneOnRunway() == null) {
						placePlane(r, plane);
						placed = true;
						System.out.println(timeCounter + ": " + plane.getType() + " Plane moved to runway");
						break;
					}
				}
				if (!placed) {
					plane.setQueueTime(timeCounter);
					if (plane.getType() == Type.LANDING){
						landingQueue.addPlaneToQueue(plane);
						System.out.println(timeCounter + ": " + plane.getType() + " Plane moved to queue");
					}
					else if (plane.getType() == Type.TAKEOFF){
						takeoffQueue.addPlaneToQueue(plane);
						System.out.println(timeCounter + ": " + plane.getType() + " Plane moved to queue");
					}
				}
				// if(plane.getType() == Type.LANDING)
				// System.out.println("Plane landing at " +
				// plane.getEventTime());
				// else if(plane.getType() == Type.TAKEOFF)
				// System.out.println("Plane taking off at " +
				// plane.getEventTime());

				// For the scoring simulation, gui = null;
				if (gui != null)
					gui.changeSchedule.removePlane(plane);
			} else if (e instanceof Weather) {
				Weather weather = (Weather) e;
				
				//Open the runways when the weather event happens, then close them later on in this
				//Function based on the event
				for(int i = 0; i < Runways.length;i++)
					Runways[i].setOpen(true);

				// Handle runway closings, and special rules.
				if (weather.getType() == WeatherType.WIND) {
					// Two runways close if the weather is wind
					Runways[3].setOpen(false);
					Runways[1].setOpen(false);
				} else if (weather.getType() == WeatherType.RAIN) {

					//No runways close for rain
				} else if (weather.getType() == WeatherType.SNOW) {
					// If it's already snowing when a snow event arrives, don't
					// reset the timer
					if (currentWeather == WeatherType.SNOW
							|| currentWeather == WeatherType.SNOW_5
							|| currentWeather == WeatherType.SNOW_10)
						continue;
					// After 5 minutes of snow, runway time increases.
					// This event will be ignored if the weather changes before
					// then.
					eventPriorityQueue.offer(new Weather(timeCounter + 5,
							WeatherType.SNOW_5));
				} else if (weather.getType() == WeatherType.SNOW_5) {
					// If the weather has changed from snow, it can't be snowing
					// for 5 minutes,
					// so check for snow before accepting the event.
					if (currentWeather != WeatherType.SNOW)
						continue;
					eventPriorityQueue.offer(new Weather(timeCounter + 5,
							WeatherType.SNOW_10));
					
					Runways[0].setOpen(false);
					Runways[1].setOpen(false);
					
				} else if (weather.getType() == WeatherType.SNOW_10) {
					// If the weather has changed from snow_5, it can't be
					// snowing for 10 minutes,
					// so check for snow before accepting the event.
					if (currentWeather != WeatherType.SNOW_5)
						continue;
					//Close all runways
					for(int i = 0; i < Runways.length;i++)
						Runways[i].setOpen(false);

				}

				currentWeather = weather.getType();
				
				//After the gui gets initialized, update the weather everytime it changes
				if (gui != null)
					gui.updateWeather(currentWeather);
				System.out.println(timeCounter + ": " + "Weather is " + currentWeather);
			} else if (e instanceof RunwayEvent) {
				System.out.println(timeCounter + ": " + "Plane removed from runway");

				RunwayEvent event = (RunwayEvent) e;
				event.getRunway().clearPlaneOnRunway();
				// Now that the runway is free, try to fill it again. Landing
				// planes first.
				if (landingQueue.getSize() > 0) {
					Plane nextPlane = landingQueue.removeFirstObject();
					placePlane(event.getRunway(), nextPlane);
					// Calculate delay
					totalDelays += (timeCounter - nextPlane.getQueueTime());
				} else if (takeoffQueue.getSize() > 0) {
					Plane nextPlane = takeoffQueue.removeFirstObject();
					placePlane(event.getRunway(), nextPlane);
					// Calculate delay
					totalDelays += (timeCounter - nextPlane.getQueueTime());
				}
			}

			// Update displayed information
			if (gui != null) { // For the scoring simulation, gui = null;
				gui.updateTakeoffQueueDisplays(takeoffQueue);
				gui.updateLandingQueueDisplays(landingQueue);
				gui.updateRunways(Runways);
				gui.score.updateScore();
			}

			e = eventPriorityQueue.peek();
			if (e == null)
				return;
		}

	}

	/**
	 * Authors: Ben Lee Purpose: Places a plane on the runway and schedules it
	 * to leave at the right time. Tested by: Tested on:
	 */
	private void placePlane(Runway r, Plane plane) {
		r.setPlane(plane);
		if (currentWeather == WeatherType.SUN)
			eventPriorityQueue
					.offer(new RunwayEvent(r, timeCounter + SUN_TIME));
		else if (currentWeather == WeatherType.RAIN)
			eventPriorityQueue
					.offer(new RunwayEvent(r, timeCounter + RAIN_TIME));
		else if (currentWeather == WeatherType.WIND)
			eventPriorityQueue
					.offer(new RunwayEvent(r, timeCounter + WIND_TIME));
		else if (currentWeather == WeatherType.SNOW)
			eventPriorityQueue.offer(new RunwayEvent(r, timeCounter
					+ SNOW_TIME_START));
		else if (currentWeather == WeatherType.SNOW_5)
			eventPriorityQueue.offer(new RunwayEvent(r, timeCounter
					+ SNOW_5_TIME));
		else if (currentWeather == WeatherType.SNOW_10)
			eventPriorityQueue.offer(new RunwayEvent(r, timeCounter
					+ SNOW_10_TIME));
	}

	/**
	 * Authors: Ben Lee Purpose: Skips to just before the next event in the
	 * queue. Tested by: Tested on:
	 */
	public void skip() {
		if (!eventPriorityQueue.isEmpty())
			timeCounter = eventPriorityQueue.peek().getEventTime() - 1;
	}

	/**
	 * Authors: Ben Lee Purpose: Copies the current simulation, runs it until
	 * the events are finished, and returns the delays. Tested by: Tested on:
	 */
	private int generateSimScore() {
		// Make a copy of the priority queue, and make a simulation with it.
		PriorityQueue<Event> queueCopy = new PriorityQueue<Event>(
				eventPriorityQueue);
		Simulation copy = new Simulation(queueCopy);
		// Run all the way through.
		while (copy.running) {
			copy.update();
		}
		return copy.totalDelays;
	}

	public int getSimScore() {
		return simScore;
	}

	public int getTotalDelays() {
		return totalDelays;
	}
}